package cn.funeralobjects.aoss.service.impl;

import cn.funeralobjects.ams.web.api.ProductApiApi;
import cn.funeralobjects.ams.web.api.RamRoleApi;
import cn.funeralobjects.ams.web.api.StsAccessKeyApi;
import cn.funeralobjects.ams.web.model.VPolicy;
import cn.funeralobjects.ams.web.model.VStatement;
import cn.funeralobjects.ams.web.model.VStsAccessKey;
import cn.funeralobjects.ams.web.model.VStsAccessKeyParam;
import cn.funeralobjects.aoss.BasicInfo;
import cn.funeralobjects.aoss.entity.EBucket;
import cn.funeralobjects.aoss.entity.EBucketRam;
import cn.funeralobjects.aoss.entity.QEBucket;
import cn.funeralobjects.aoss.entity.QEBucketRam;
import cn.funeralobjects.aoss.enums.BucketAction;
import cn.funeralobjects.aoss.enums.StorageClass;
import cn.funeralobjects.aoss.model.Bucket;
import cn.funeralobjects.aoss.model.BucketRam;
import cn.funeralobjects.aoss.model.BucketStsAccessKey;
import cn.funeralobjects.aoss.repository.EBucketRamRepository;
import cn.funeralobjects.aoss.repository.EBucketRepository;
import cn.funeralobjects.aoss.service.BucketService;
import cn.funeralobjects.aoss.utils.BucketUtils;
import cn.funeralobjects.common.jpa.service.CommonCloudCrudJpaService;
import cn.funeralobjects.common.repository.CommonCloudRepository;
import cn.funeralobjects.common.service.annotataion.CommonRollback;
import cn.funeralobjects.common.service.annotataion.CommonService;
import cn.funeralobjects.common.service.exception.DataConflictException;
import cn.funeralobjects.common.service.exception.LackDataException;
import cn.funeralobjects.common.service.validate.DataCheck;
import cn.funeralobjects.util.Assert;
import cn.funeralobjects.util.NumberUtils;
import cn.funeralobjects.util.annotation.*;
import cn.funeralobjects.utils.aliyun.oss.OssAction;
import cn.funeralobjects.utils.aliyun.oss.model.GetSimplifiedObjectMetaParam;
import cn.funeralobjects.utils.aliyun.oss.model.HeadObjectParam;
import cn.funeralobjects.utils.aliyun.oss.model.ListBucketsParam;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.SimplifiedObjectMeta;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.JPQLQueryFactory;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

/**
 * @author FuneralObjects
 * Create date: 2020/6/1 11:18 AM
 */
@CommonService
public class BucketServiceImpl implements BucketService, CommonCloudCrudJpaService<Bucket, EBucket, Integer> {

    @Resource
    private EBucketRepository eBucketRepository;

    @Resource
    private ProductApiApi productApiApi;

    @Resource
    private RamRoleApi ramRoleApi;

    @Resource
    private StsAccessKeyApi stsAccessKeyApi;

    @Resource
    private EBucketRamRepository eBucketRamRepository;

    @Resource
    private JPQLQueryFactory jpqlQueryFactory;

    private static final String POLICY_RESOURCE_FORMAT = "acs:oss:*:*:%s/%s";

    private static final String POLICY_EFFECT_ALLOW = "Allow";

    @Override
    public CommonCloudRepository<EBucket, Integer> commonCloudRepository() {
        return eBucketRepository;
    }

    @AssertArg
    @Override
    public void checkConflict(@AssertArg Bucket bucket) throws DataConflictException {
        DataCheck.conflictData(this.existsNameInProdRes(bucket.getProdResCode(), bucket.getName()), () -> createDataCode("prodResCode-name", bucket.getProdResCode() + "-" + bucket.getName()));
    }

    @Override
    public EBucket toEntity(Bucket bucket) {
        return Optional.ofNullable(bucket)
                .map(modify -> {
                    EBucket entity = new EBucket();
                    entity.setCreateDate(modify.getCreateDate())
                            .setExtranetEndpoint(modify.getExtranetEndpoint())
                            .setIntranetEndpoint(modify.getIntranetEndpoint())
                            .setLocation(modify.getLocation())
                            .setProdResCode(modify.getProdResCode())
                            .setStorageClass(modify.getStorageClass())
                            .setName(modify.getName())
                            .setCode(modify.getCode());
                    return entity;
                }).orElse(null);
    }

    @AssertArg
    @Override
    public void checkConflict(@AssertArg Bucket bucket, @ArgNotZero Integer withoutId) throws DataConflictException {
        DataCheck.conflictData(this.existsNameInProdRes(bucket.getProdResCode(), bucket.getName(), withoutId), () -> createDataCode("prodResCode-name", bucket.getProdResCode() + "-" + bucket.getName()));
    }

    @Override
    public void copyToEntity(Bucket sourceModify, EBucket targetEntity) {
        targetEntity.setCreateDate(sourceModify.getCreateDate())
                .setExtranetEndpoint(sourceModify.getExtranetEndpoint())
                .setIntranetEndpoint(sourceModify.getIntranetEndpoint())
                .setLocation(sourceModify.getLocation())
                .setProdResCode(sourceModify.getProdResCode())
                .setStorageClass(sourceModify.getStorageClass())
                .setName(sourceModify.getName())
                .setCode(sourceModify.getCode());
    }

    @Override
    public boolean isValid(Integer id) {
        return NumberUtils.notZero(id);
    }

    @Override
    public void validateModify(Bucket bucket) {
        Assert.argAssert(bucket, "bucket");
    }

    @AssertArg
    @Override
    public boolean existsNameInProdRes(@ArgHasLength String prodResCode, @ArgHasLength String name) {
        return eBucketRepository.existsAllByProdResCodeAndNameAndDelSalt(prodResCode, name, getExistsDelSalt());
    }

    @AssertArg
    @Override
    public Optional<String> getNameById(@ArgNotZero Integer id) {
        return eBucketRepository.getNameByIdAndDelSalt(id, getExistsDelSalt());
    }

    @AssertArg
    @CommonRollback
    @Override
    public void bindRamRole(@ArgNotZero Integer id, @ArgHasLength String ramRoleCode) {
        this.mustExistsById(id);
        eBucketRamRepository.save(new EBucketRam().setBucketId(id).setRamRoleCode(ramRoleCode));
    }

    @AssertArg
    @Override
    public boolean existsNameInProdRes(@ArgHasLength String prodResCode, @ArgHasLength String name, @ArgNotZero Integer withoutId) {
        return eBucketRepository.existsAllByProdResCodeAndNameAndDelSaltAndIdNot(prodResCode, name, getExistsDelSalt(), withoutId);
    }

    @AssertArg
    @Override
    public Optional<EBucket> findOneByNameInProdRes(@ArgHasLength String prodRes, @ArgHasLength String name) {
        return eBucketRepository.findFirstByProdResCodeAndNameAndDelSalt(prodRes, name, getExistsDelSalt());
    }

    @CommonRollback
    @Override
    public List<EBucket> addIfNotExistsInBatch(@ArgNotEmpty List<Bucket> buckets) {
        return buckets.stream().filter(Objects::nonNull).map(modify -> findOneByNameInProdRes(modify.getProdResCode(), modify.getName()).orElseGet(() -> this.add(modify))).collect(Collectors.toList());
    }

    @CommonRollback
    @AssertArg
    @Override
    public BucketStsAccessKey createStsAccessKey(@ArgNotZero Integer id, @ArgNotZero Integer stsProdResId, @ArgNonNull BucketAction action, @ArgHasLength String path, Long duration) {
        AtomicReference<VStsAccessKey> stsAccessKey = new AtomicReference<>(null);
        BucketRam bucketRam = this.findRamById(id).orElseThrow(() -> new LackDataException(String.format("The RamRole of Bucket[%s] doesn't exists!", id), createDataCode("id", String.valueOf(id))));
        VPolicy policy = new VPolicy()
                .setVersion("1")
                .setStatement(List.of(new VStatement()
                        .setResource(List.of(createPolicyResourceStr(bucketRam.getBucketName(), path)))
                        .setAction(List.of(action.getAction()))
                        .setEffect(POLICY_EFFECT_ALLOW)
                ));
        ramRoleApi.getIdByCode(bucketRam.getRamRoleCode()).flatMap(ramRoleId -> {
            return stsAccessKeyApi.createStsAccessKey(new VStsAccessKeyParam()
                    .setPolicy(policy)
                    .setDuration(duration)
                    .setRoleSessionName(BasicInfo.MODULE_NAME)
                    .setRoleId(ramRoleId)
                    .setProdResId(stsProdResId));
        }).subscribe(stsAccessKey::set);
        return new BucketStsAccessKey().setStsAccessKey(stsAccessKey.get()).setRegionId(bucketRam.getRegionId()).setBucketName(bucketRam.getBucketName());
    }

    @Override
    public SimplifiedObjectMeta getSimplifiedObjectMeta(Integer id, String objectPath) {
        EBucket bucket = this.findById(id).orElseThrow(() -> new LackDataException(String.format("The Bucket[%s] doesn't exists!", id), createDataCode("id", String.valueOf(id))));
        AtomicReference<SimplifiedObjectMeta> simplifiedObjectMeta = new AtomicReference<>(null);
        BucketUtils.getOssClient(bucket.getProdResCode(), OssAction.GET_SIMPLIFIED_OBJECT_META, productApiApi, bucket.getLocation())
                .map(aliyunOssClient -> aliyunOssClient.doActionAndClose(new GetSimplifiedObjectMetaParam(bucket.getName(), objectPath)).getResult()).subscribe(simplifiedObjectMeta::set);
        return simplifiedObjectMeta.get();
    }


    @Override
    public ObjectMetadata headObjectMeta(Integer id, String objectPath) {
        EBucket bucket = this.findById(id).orElseThrow(() -> new LackDataException(String.format("The Bucket[%s] doesn't exists!", id), createDataCode("id", String.valueOf(id))));
        AtomicReference<ObjectMetadata> objectMetaData = new AtomicReference<>(null);
        BucketUtils.getOssClient(bucket.getProdResCode(), OssAction.GET_SIMPLIFIED_OBJECT_META, productApiApi, bucket.getLocation())
                .map(aliyunOssClient -> aliyunOssClient.doActionAndClose(new HeadObjectParam(bucket.getName(), objectPath)).getResult()).subscribe(objectMetaData::set);
        return objectMetaData.get();
    }

    @Override
    public Optional<BucketRam> findRamById(Integer id) {
        Tuple tuple = jpqlQueryFactory.select(QEBucket.eBucket.id,
                QEBucket.eBucket.name,
                QEBucket.eBucket.location,
                QEBucketRam.eBucketRam.ramRoleCode)
                .from(QEBucket.eBucket)
                .join(QEBucketRam.eBucketRam).on(QEBucketRam.eBucketRam.bucketId.eq(QEBucket.eBucket.id))
                .where(
                        QEBucket.eBucket.id.eq(id)
                                .and(QEBucket.eBucket.delSalt.eq(getExistsDelSalt()))
                ).fetchOne();

        return Optional.ofNullable(tuple)
                .map(t -> new BucketRam()
                        .setRegionId(t.get(QEBucket.eBucket.location))
                        .setBucketId(t.get(QEBucket.eBucket.id))
                        .setBucketName(t.get(QEBucket.eBucket.name))
                        .setRamRoleCode(t.get(QEBucketRam.eBucketRam.ramRoleCode)));
    }

    @CommonRollback
    @Override
    public Mono<List<EBucket>> syncFromCloudByProductRes(String prodResCode) {
        return BucketUtils.getOssClient(prodResCode, OssAction.LIST_BUCKETS, productApiApi)
                .map(client -> {
                    List<Bucket> buckets = client.doActionAndClose(new ListBucketsParam()).getResult()
                            .stream()
                            .map(bucket -> new Bucket()
                                    .setCode(UUID.randomUUID().toString())
                                    .setCreateDate(bucket.getCreationDate())
                                    .setExtranetEndpoint(bucket.getExtranetEndpoint())
                                    .setIntranetEndpoint(bucket.getIntranetEndpoint())
                                    .setLocation(bucket.getLocation())
                                    .setStorageClass(StorageClass.of(bucket.getStorageClass().toString()))
                                    .setProdResCode(prodResCode)
                                    .setName(bucket.getName()))
                            .collect(Collectors.toList());
                    return this.addIfNotExistsInBatch(buckets);
                });
    }

    private Integer getExistsDelSalt() {
        return (Integer) eBucketRepository.getDefaultExistsDelSalt();
    }

    private static String createPolicyResourceStr(String bucketName, String path) {
        return String.format(POLICY_RESOURCE_FORMAT, bucketName, path);
    }
}
